﻿using System;
using System.Collections;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Xml.XPath;
using ErrorDumper.Dumping;

namespace ErrorDumper {
    public static class ExceptionSerializer {
        #region Save

        static readonly string _extension = ".exception";

        static string CreateFileName() {
            return DateTime.UtcNow.ToString("yyyy-MM-dd HH-mm-ss-fff") + _extension;
        }

        public static string Save(ExceptionPropertyCollection properties, Exception exception, string directory) {
            if (exception == null)
                throw new ArgumentNullException("exception");
            if (directory == null)
                throw new ArgumentNullException("directory");

            if (!Directory.Exists(directory))
                Directory.CreateDirectory(directory);

            string fileName = Path.Combine(directory, CreateFileName());

            IDictionary _registry = new HybridDictionary();
            using (FileStream fs = File.OpenWrite(fileName))
            using (XmlWriter xw = new XmlTextWriter(fs, Encoding.UTF8)) {
                SerializeException(properties, exception, ExceptionSerializationOptions.MaximumSerialization, xw, _registry);
                xw.Flush();
            }
            _registry.Clear();
            GC.SuppressFinalize(_registry);

            return fileName;
        }

        public static string Save(ExceptionPropertyCollection properties, char[] content, string directory) {
            if (directory == null)
                throw new ArgumentNullException("directory");
            if (!Path.IsPathRooted(directory))
                throw new ArgumentException("Directory to save must be rooted");

            if (!Directory.Exists(directory))
                Directory.CreateDirectory(directory);

            string fileName = Path.Combine(directory, CreateFileName());

            using (FileStream fs = File.OpenWrite(fileName))
            using (StreamWriter wr = new StreamWriter(fs, Encoding.UTF8))
                if (content != null && content.Length > 0)
                    wr.Write(content);

            return fileName;
        }

        public static string Save(ExceptionPropertyCollection properties, string content, string directory) {
            if (directory == null)
                throw new ArgumentNullException("directory");
            if (!Path.IsPathRooted(directory))
                throw new ArgumentException("Directory to save must be rooted");

            if (!Directory.Exists(directory))
                Directory.CreateDirectory(directory);

            string fileName = Path.Combine(directory, CreateFileName());

            using (FileStream fs = File.OpenWrite(fileName))
            using (StreamWriter wr = new StreamWriter(fs, Encoding.UTF8))
                if (content != null && content.Length > 0)
                    wr.Write(content);

            return fileName;
        }

        #endregion

        #region DeserializeProperties

        static readonly XPathExpression _propertiesXPath = XPathExpression.Compile("/Exception/Properties/*");

        public static ExceptionPropertyCollection DeserializeProperties(string content) {
            if (string.IsNullOrEmpty(content))
                return null;
            using (StringReader sr = new StringReader(content))
                return DeserializeProperties(new XPathDocument(sr).CreateNavigator());
        }

        public static ExceptionPropertyCollection DeserializeProperties(Stream input) {
            if (input == null)
                throw new ArgumentNullException("input");
            return DeserializeProperties(new XPathDocument(input).CreateNavigator());
        }

        public static ExceptionPropertyCollection DeserializeProperties(XmlReader reader) {
            if (reader == null)
                throw new ArgumentNullException("reader");
            return DeserializeProperties(new XPathDocument(reader).CreateNavigator());
        }

        public static ExceptionPropertyCollection DeserializeProperties(TextReader reader) {
            if (reader == null)
                throw new ArgumentNullException("reader");
            return DeserializeProperties(new XPathDocument(reader).CreateNavigator());
        }

        public static ExceptionPropertyCollection DeserializeProperties(XPathDocument doc) {
            if (doc == null)
                throw new ArgumentNullException("doc");
            return DeserializeProperties(doc.CreateNavigator());
        }

        public static ExceptionPropertyCollection DeserializeProperties(XPathNavigator navigator) {
            if (navigator == null)
                throw new ArgumentNullException("navigator");
            ExceptionPropertyCollection properties = null;
            XPathNodeIterator it = navigator.Select(_propertiesXPath);
            while (it.MoveNext()) {
                if (properties == null)
                    properties = new ExceptionPropertyCollection();
                XPathNavigator _current = it.Current;
                string __name = ExceptionProperty.DecodeName(_current.Name);
                properties[__name] = _current.Value;
            }
            return properties;
        }

        #endregion

        #region SerializeException

        public static string SerializeException(ExceptionPropertyCollection properties, Exception exception,
            ExceptionSerializationOptions options) {
            using (StringWriter sw = new StringWriter())
            using (XmlTextWriter xw = new XmlTextWriter(sw)) {
                SerializeException(properties, exception, options, xw);
                xw.Flush();
                return sw.ToString();
            }
        }

        public static void SerializeException(ExceptionPropertyCollection properties, Exception exception,
            ExceptionSerializationOptions options, XmlWriter writer) {
            IDictionary _registry = new HybridDictionary();
            SerializeException(properties, exception, options, writer, _registry);
            _registry.Clear();
            GC.SuppressFinalize(_registry);
        }

        public static void SerializeException(ExceptionPropertyCollection properties, Exception exception,
            ExceptionSerializationOptions options, XmlWriter writer, IDictionary registry) {
            if (registry == null)
                throw new ArgumentNullException("registry");

            if (exception == null)
                exception = new Exception("Unknown exception");

            if ((options & ExceptionSerializationOptions.SerializeContent) > 0
                && (options & ExceptionSerializationOptions.SerializeExtendedDump) > 0) {
                Exception current = exception;
                while (current != null) {
                    var extendedDump = current as IExtendedDumpAccessor;
                    try {
                        if (extendedDump != null)
                            current.Data["ExtendedDump"] = extendedDump.GetExtendedDump();
                    }
                    catch (Exception buldDumpException) {
                        try {
                            current.Data["__ResolveExtendedDumpException__"] =
                                SerializeException(null, buldDumpException,
                                options ^ ExceptionSerializationOptions.SerializeExtendedDump);
                        }
                        catch { }
                    }
                    current = current.InnerException;
                }
            }

            if ((options & ExceptionSerializationOptions.SerializeProperties) > 0
                && (options & ExceptionSerializationOptions.BuildProperties) > 0)
                try {
                    if (properties == null)
                        properties = new ExceptionPropertyCollection();
                    ExceptionPropertyCollection.Build(exception, properties, false);
                }
                catch (Exception extendPropertiesException) {
                    try {
                        exception.Data["__BuildPropertiesException__"] =
                            SerializeException(null, extendPropertiesException,
                            options ^ ExceptionSerializationOptions.BuildProperties);
                    }
                    catch { }
                }

            if ((options & ExceptionSerializationOptions.SafeMode) > 0) {
                using (MemoryStream ms = new MemoryStream())
                using (XmlWriter msw = new XmlTextWriter(ms, Encoding.UTF8)) {
                    try {
                        SerializeException(properties, exception, options ^ ExceptionSerializationOptions.SafeMode, msw, registry);
                        msw.Flush();
                    }
                    catch (Exception serializationException) {
                        writer.WriteStartElement("Exception");

                        writer.WriteStartElement("OriginalException");
                        if (exception != null)
                            try { writer.WriteString(exception.ToString()); }
                            catch { }
                        writer.WriteEndElement();

                        writer.WriteStartElement("SerializationException");
                        if (serializationException != null)
                            try { writer.WriteString(serializationException.ToString()); }
                            catch { }
                        writer.WriteEndElement();

                        writer.WriteStartElement("SerializationPiece");
                        if (ms.Length > 0)
                            try { writer.WriteCData(Encoding.UTF8.GetString(ms.GetBuffer(), 0, (int)ms.Length)); }
                            catch { }
                        writer.WriteEndElement();

                        writer.WriteEndElement();
                        return;
                    }
                    ms.Position = 0;
                    using (XmlReader msr = new XmlTextReader(ms))
                        writer.WriteNode(msr, true);
                }
                return;
            }

            writer.WriteStartElement("Exception");

            if (properties != null && (options & ExceptionSerializationOptions.SerializeProperties) > 0) {
                writer.WriteStartElement("Properties");
                foreach (var _property in properties) {
                    writer.WriteStartElement(ExceptionProperty.EncodeName(_property.Key.ToString()));
                    writer.WriteString(_property.Value);
                    writer.WriteEndElement();
                }
                writer.WriteEndElement();
            }

            if ((options & ExceptionSerializationOptions.SerializeAssemblies) > 0) {
                writer.WriteStartElement("Assemblies");
                StringBuilder _sb = null;
                var assemblies = from assembly in AppDomain.CurrentDomain.GetAssemblies()
                                 where assembly != null
                                 let name = assembly.GetName()
                                 where name != null
                                 orderby name.Name
                                 select name;
                foreach (var assembly in assemblies) {
                    writer.WriteStartElement("Assembly");
                    writer.WriteAttributeString("Name", assembly.Name);
                    writer.WriteAttributeString("Version", assembly.Version.ToString());
                    if ((assembly.Flags & AssemblyNameFlags.PublicKey) == AssemblyNameFlags.PublicKey) {
                        byte[] _publicKeyToken = assembly.GetPublicKeyToken();
                        if (_publicKeyToken != null && _publicKeyToken.Length > 0) {
                            if (_sb == null)
                                _sb = new StringBuilder();
                            else
                                _sb.Length = 0;
                            for (int i = 0; i < _publicKeyToken.Length; i++)
                                _sb.Append(Convert.ToString(_publicKeyToken[i], 16));
                            writer.WriteAttributeString("PublicKeyToken", _sb.ToString());
                        }
                    }
                    writer.WriteEndElement();
                }
                writer.WriteEndElement();
            }

            if ((options & ExceptionSerializationOptions.SerializeContent) > 0) {
                writer.WriteStartElement("Content");
                DumpSerializer.Serialize(exception, writer, registry);
                writer.WriteEndElement();
            }

            writer.WriteEndElement();
        }

        #endregion

    }
}
